package middleware;

/**
 * Multicast project Bidois Morgan & Thomas Nunes
 *
 */
import Engine.Header;
import Engine.Message;
import Engine.TypeMessage;
import static Engine.TypeMessage.*;
import Test.I_RecvMsgHandler;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * The client multicast layer
 *
 * @author Thomas
 */
public class ClientMulticast extends AbstractMulticast implements I_ClientMulticast, I_RecvMulticastHandler {

    private InetAddress address;
    private int port;
    private ArrayList<Message> ackWithoutMessage = new ArrayList<>();
    private BurstManager burstManager = null;

    public ClientMulticast(I_RecvMsgHandler pere) {
        try {
            InetAddress addr = InetAddress.getByName("localhost");
            this.InitializeAsClient(addr, 9090, pere, this);

        } catch (IOException ex) {
            Logger.getLogger(ClientMulticast.class.getName()).log(Level.SEVERE, "Erreur Client", ex);
        }
    }

    /**
     * searching the name the client entered
     *
     * @param dir the directory
     */
    private void setIdFromDirectory(Directory dir) {
        for (ClientInfos clientInfo : dir.getClientList()) {
            if (clientInfo.getName().equals(this.name) || clientInfo.getIdClient() == this.idClient) {
                this.address = clientInfo.getAdrClient();
                this.port = clientInfo.getPort();
            }
        }
    }

    @Override
    public void setDirectory(byte[] buff) {
        String strData = new String(buff);

        //Method Split
        String[] clients = strData.split("\\|");
        int nbClient = clients.length;
        for (int i = 0; i < nbClient; i++) {
            String[] infoClient = clients[i].split(":");
            String strId = infoClient[0];
            String strName = infoClient[1];
            String strAdrWithHost = infoClient[2];
            //the adress is with his hostname, so we can't directly do the Inet.getByAddress...
            String[] adress = strAdrWithHost.split("\\/");
            //we recover the interger of the adress
            String strHost = adress[0]; //sert à rien mais pour mieux comprendre pourquoi on commence à 1
            String strAdr = adress[1];
            String strPort = infoClient[3];
            String strStatus = infoClient[4];
            try {
                int idLoc = Integer.parseInt(strId);
                int portLoc = Integer.parseInt(strPort);
                ClientInfos clInf = new ClientInfos(idLoc, strName, InetAddress.getByName(strAdr), portLoc);
                directory.addClient(clInf);
            } catch (UnknownHostException ex) {
                Logger.getLogger(ClientMulticast.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        setIdFromDirectory(directory);
    }

    @Override
    public void createSocketServer() {
        engine.createSocketServer(address, port);
        sayIAmReadyForConnection();
    }

    private void sayIAmReadyForConnection() {
        engine.send("".getBytes(), TypeMessage.CLIENT_SERVERSOCKET_READY, idClient, this.logicalClock);
    }

    /**
     * Send to all a broadcast message
     *
     * @param data
     * @param type
     */
    public void sendMsg2All(byte[] data, TypeMessage type) {
        this.logicalClock++;
        String msg = name + " - Clock " + this.logicalClock + " : " + new String(data);
        Message message;
        message = new Message(new Header(type, this.logicalClock, this.idClient), msg.getBytes());
        //Create the message in history
        try {
            this.historyMessage.add(new DataMulticast(message, false, directory.sizeAliveClient()));
        } catch (MessageWaitingOverflowException ex) {
            Logger.getLogger(ClientMulticast.class.getName()).log(Level.SEVERE, "OVERFLOW");
            this.historyMessage = new DataMulticastManager();
            this.deliver(new Message(new Header(TypeMessage.NORMAL, 1, 1), ("##Vous avez été déconnecté...Reconexion ##").getBytes()));

        }
        this.send2All(msg.getBytes(), type);
        //ACK this
        this.sendACK2All(logicalClock, idClient);
    }

    /**
     * Connection to clients with index superior to it
     */
    @Override
    public void connectToGroup() {
        ClientInfos client = directory.getClientById(this.idClient);
        int indexClient = directory.getClientList().indexOf(client);

        if (indexClient > 0) {
            indexClient--;
            while (indexClient >= 0) {
                ClientInfos ciToConnect = directory.getClientAt(indexClient);
                try {
                    SocketChannel socket = engine.connectClientToAnotherClient(ciToConnect.getAdrClient(), ciToConnect.getPort(), ciToConnect.getIdClient());
                    ciToConnect.setSocketChannel(socket);
                } catch (Exception ex) {
                    Logger.getLogger(ClientMulticast.class.getName()).log(Level.WARNING, "Fail connect", ex);
                }
                indexClient--;

            }
        }
    }

    @Override
    public void receivedCB(Message message, SocketChannel socketChannel) {
        int lcSend = message.getLogicalClock();
        int idClSend = message.getIdClient();
        switch (message.getHeader().getType()) {
            case NORMAL:
                try {
                    this.historyMessage.add(new DataMulticast(message, false, directory.sizeAliveClient()));
                } catch (MessageWaitingOverflowException ex) {
                    Logger.getLogger(ClientMulticast.class.getName()).log(Level.SEVERE, "OVERFLOW");
                    this.historyMessage = new DataMulticastManager();
                    this.deliver(new Message(new Header(TypeMessage.NORMAL, 1, 1), ("##Vous avez été déconnecté...Reconexion ##").getBytes()));

                }
//                System.out.println("historyMessage" + this.historyMessage.toString());
                this.sendACK2All(lcSend, idClSend);
                if (!this.ackWithoutMessage.isEmpty()) {
                    Object[] listAFK = ackWithoutMessage.toArray();
                    for (Object mess : listAFK) {
                        if (mess != null) {
                            this.addNewAck((Message) mess);
                        }
                    }
                }
                break;
            case DIE:
                System.exit(1);
                break;
            case GIVE_ME_A_MESSAGE:
                this.deliverDebugMessage(message);
                this.engine.send(directory.getDirectoryToSend().getBytes(), TypeMessage.TAKE_MY_HISTORIC, this.idClient, this.logicalClock);
                break;
            case NEW_CONNECTION:
                this.deliverDebugMessage(message);
                break;
            case SOMEONE_LEAVE:
                this.deliverDebugMessage(message);
                int afkClientId = Integer.parseInt(new String(message.getContent()));
                this.removeAclient(afkClientId);
                //this.engine.send(data, ACK, idClient, lcSend);
                break;
            case START_BURST:
                System.out.println("burstmode: " + this.burstmode);
                if (!this.burstmode) {
                    this.send2All("".getBytes(), TypeMessage.START_BURST);
                    this.deliverDebugMessage(message);
                    this.burstManager = new BurstManager(this);
                    this.burstManager.start();
                    this.burstmode = true;
                }
                break;
            case STOP_BURST:
                if (this.burstmode) {
                    this.send2All("".getBytes(), TypeMessage.STOP_BURST);
                    this.deliverDebugMessage(message);
                    this.burstManager.kill();
                    this.burstmode = false;
                }
                break;
            case ACK:
                if (message.getHeader().getType() == TypeMessage.ACK) {

                    if (!this.addNewAck(message)) {
                        /**
                         * J'ai pas reçu ce message acquité TODO: demander le
                         * message a celui qui a fournit l'ACK
                         */
                        //test si client est mort
                        //sinon on attend son message qui va venir
                        ackWithoutMessage.add(message);
                    }
                }
                break;
            case SEND_DIRECTORY:
                this.setDirectory(message.getContent());
                this.createSocketServer();
                this.deliverDebugMessage(message);
                break;
            case SEND_ID:
                this.name += this.idClient;
                this.deliverDebugMessage(message);
                break;
            case BEGIN_CONNECTION:
                //Client get the right to start the connection
                this.deliver(new Message(new Header(TypeMessage.NORMAL, 1, 1), ("Vous êtes connecté sous le nom:" + name).getBytes()));
                this.connectToGroup();
                this.deliverDebugMessage(message);
                break;
        }

        if (lcSend > this.logicalClock) {
            this.logicalClock = lcSend + 1;
        }
    }

    /**
     * Send a ack to all client
     *
     * @param lcSend the logical clock of original message
     * @param idClSend the id of original message
     */
    private void sendACK2All(int lcSend, int idClSend) {
        ByteBuffer buff = ByteBuffer.allocate(4 + 4);
        buff.putInt(lcSend);
        buff.putInt(idClSend);
        //We auto -ACK
        this.receivedCB(new Message(new Header(ACK, this.logicalClock, this.idClient), buff.array()), null);
        //We ACK others
        this.send2All(buff.array(), TypeMessage.ACK);
    }

    /**
     * Add a new ack to a message
     *
     * @param message the ACK message
     * @return if we find the coresponding message
     */
    private boolean addNewAck(Message message) {
        ByteBuffer content = ByteBuffer.wrap(message.getContent());
        int idClSend = message.getIdClient();
        int lCack = content.getInt();
        int iDack = content.getInt();
        boolean canDeliver = false;
        if (this.historyMessage.contain(lCack, iDack)) {
            canDeliver = this.historyMessage.addACK(lCack, iDack, idClSend);
        }
        ArrayList<DataMulticast> message2deliver = this.historyMessage.getAllMessageCanBeDeliver();
        for (DataMulticast messageSaved : message2deliver) {
            if (messageSaved != null && !messageSaved.isDeliver()) {
                this.deliver(messageSaved.getMessage());
                messageSaved.setDeliver(true);
                this.historyMessage.remove(lCack, iDack);
            }
        }
        return canDeliver;
    }

    public String getAllClientName() {
        return this.directory.getAllClientName();
    }
}
